package fr.cephb.dbsnptools;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import com.sleepycat.bind.tuple.IntegerBinding;
import com.sleepycat.bind.tuple.TupleBinding;
import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.bind.tuple.TupleOutput;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;

import fr.cephb.berkeleydb.db.DuplicateDB;
import fr.cephb.berkeleydb.db.Walker;
import fr.cephb.io.IOUtils;
import fr.cephb.util.AbstractCmdLine;

public class DigestBowtie
	extends AbstractCmdLine
	{
	/** pattern for a simple mutation */
	private final static  Pattern TAB=Pattern.compile("[\t]");
	  
	private static class Hit
		{
		int rs_id;
		char strand;
		int chrom_id;
		int position;
		int length;
		String  mismatch="";
		
		int countMismatches()
			{
			int c=0;
			for(int i=0;i< this.mismatch.length();++i)
				{
				if(this.mismatch.charAt(i)==',') c++;
				}
			return c;
			}

		@Override
		public int hashCode()
			{
			final int prime = 31;
			int result = 1;
			result = prime * result + chrom_id;
			result = prime * result + length;
			result = prime * result + position;
			result = prime * result + rs_id;
			result = prime * result + strand;
			return result;
			}

		@Override
		public boolean equals(Object obj)
			{
			if (this == obj) return true;
			if (obj == null) return false;
			if (getClass() != obj.getClass()) return false;
			Hit other = (Hit) obj;
			if (chrom_id != other.chrom_id) return false;
			if (length != other.length) return false;
			if (position != other.position) return false;
			if (rs_id != other.rs_id) return false;
			if (strand != other.strand) return false;
			return true;
			}
		
		}
	
	private static class CandidatePair
	implements Comparable<CandidatePair>
		{
		Hit L;
		Hit R;
		
		CandidatePair(Hit L,Hit R)
			{
			this.R=R;
			this.L=L;
			}
		
		public int countMismatches()
			{
			return L.countMismatches()+R.countMismatches();
			}
		
		@Override
		public boolean equals(Object o)
			{
			if(o==this) return true;
			if(o==null || !(o instanceof CandidatePair)) return false;
			CandidatePair cp=CandidatePair.class.cast(o);
			return R.equals(cp.R) && L.equals(cp.L);
			}
		
		@Override
		public int hashCode()
			{
			return R.hashCode()+31*L.hashCode();
			}
		
		@Override
		public int compareTo(CandidatePair o)
			{
			return countMismatches()-o.countMismatches();
			}
		@Override
		public String toString()
			{
			return " L :"+L+"\n R :"+R;
			}
		public int getRsId()
			{
			if(R.rs_id!=L.rs_id) throw new RuntimeException("getRsId:"+R.rs_id+"/"+L.rs_id);
			return this.R.rs_id;
			}
		
		public int getChromId()
			{
			if(R.chrom_id!=L.chrom_id) throw new RuntimeException("getChromId:"+toString());
			return this.R.chrom_id;
			}
		
		public char getStrand()
			{
			assert(R.strand==L.strand);
			return this.R.strand;
			}
		
		public int getSNPStart()
			{
			return getStrand()=='+'
				?L.position+ L.length
				:R.position+ R.length
				;
				
			}
		
		public int getSNPEnd()
			{
			return getStrand()=='+'
				?R.position
				:L.position
				;
				
			}
		void print(PrintStream out)
			{
			out.print("rs"+getRsId()+"\t"+
					int2chrom(getChromId())+"\t"+
					getStrand()+"\t"+
					getSNPStart()+"\t"+
					getSNPEnd()
					);
			}
		}
	
	
	private static class HitBinding
		extends TupleBinding<Hit>
		{
		@Override
		public Hit entryToObject(TupleInput in)
			{
			Hit h= new Hit();
			h.rs_id=in.readInt();
			h.strand=in.readChar();
			h.chrom_id=in.readInt();
			h.position=in.readInt();
			h.length=in.readInt();
			h.mismatch=in.readString();
			return h;
			}
		@Override
		public void objectToEntry(Hit h, TupleOutput out)
			{
			out.writeInt(h.rs_id);
			out.writeChar(h.strand);
			out.writeInt(h.chrom_id);
			out.writeInt(h.position);
			out.writeInt(h.length);
			out.writeString(h.mismatch);
			}
		}
	
	private static class HitDatabase
		extends DuplicateDB<Integer, Hit>
		{
		public HitDatabase(Environment env,DatabaseConfig cfg,String name)
			{
			super(env,null,name,cfg,new IntegerBinding(),new HitBinding());
			}
		}
	
	private File tmpDir;
	private Environment environment=null;
	private HitDatabase left=null;
	private HitDatabase right=null;
	private int max_len_deletion=100;
	private int expect_flanking_length=120;
	
	private void open() throws DatabaseException,IOException
		{
		this.tmpDir=IOUtils.createTempDir("_digestBowtie",".dir",
				new File(System.getProperty("user.dir"))
				);
		LOG.info("created "+this.tmpDir);
		
		LOG.info("open env");
		EnvironmentConfig envConfig= new EnvironmentConfig();
		envConfig.setAllowCreate(true);
		envConfig.setReadOnly(false);
		envConfig.setConfigParam(EnvironmentConfig.LOG_FILE_MAX,"500000000"/* 500 Mo */);
		this.environment= new Environment(this.tmpDir, envConfig);
		for(int i=0;i< 2;++i)
			{
			LOG.info("open side "+i);
			DatabaseConfig cfg= new DatabaseConfig();
			cfg.setAllowCreate(true);
			cfg.setReadOnly(false);
			cfg.setSortedDuplicates(true);
			HitDatabase db=new HitDatabase(this.environment, cfg, "hit"+(i==0?"L":"R"));
			if(i==0)
				{
				this.left=db;
				}
			else
				{
				this.right=db;
				}
			}
		
		}
	
	private void close()
		{
		if(this.left!=null)
			{
			this.left.close();
			}
		
		if(this.right!=null)
			{
			this.right.close();
			}
		
		if(this.environment!=null)
			{
			this.environment.close();
			}
		if(this.tmpDir!=null)
			{
			for(File f:this.tmpDir.listFiles())
				{
				f.delete();
				}
			this.tmpDir.delete();
			this.tmpDir=null;
			}
		}
	private static int chrom2int(String s)
		{
		if(s.equals("X")) return 23;
		if(s.equals("Y")) return 24;
		if(s.equals("M")) return 25;
		return Integer.parseInt(s);
		}
	
	private static String int2chrom(int i)
		{
		switch(i)
			{
			case 23: return "X";
			case 24: return "Y";
			case 25: return "M";
			default: return String.valueOf(i);
			}
		}
	
	private void parse(BufferedReader in) throws IOException
		{
		int nLine=0;
		String token[];
		String line=null;
		while((line=in.readLine())!=null)
			{
			nLine++;
			if(nLine%1000000==0)
				{
				System.err.println("lines:"+nLine+" ..."+line);
				}
			token=TAB.split(line);
			Hit h=new Hit();
			
			h.length= Integer.parseInt(token[4]);
			if(h.length!=expect_flanking_length)
				{
				continue;
				}
			
			char side=token[0].charAt(0);
			assert(side=='L' || side=='R');
			h.rs_id= Integer.parseInt(token[0].substring(1));
			h.strand= token[1].charAt(0);
			assert(h.strand=='+' || h.strand=='-');
			h.chrom_id=chrom2int(token[2]);
			h.position= Integer.parseInt(token[3]);
			
			
			if(token.length>5)
				{
				h.mismatch=token[5];
				}
			HitDatabase db=(side=='L'?left:right);
			db.put(null, h.rs_id, h);
			}
		}
	
	private void dump()
		{
		Walker<Integer, Hit> c= this.left.openWalker(null);
		List<Hit> leftHit=new ArrayList<Hit>();
		List<Hit> rightHit=new ArrayList<Hit>();
		while(c.nextNoDup())
			{
			leftHit.clear();
			rightHit.clear();
			int rs_id = c.getKey();
			leftHit.addAll(this.left.get(null, rs_id));
			rightHit.addAll(this.right.get(null, rs_id));
			
			System.out.println("#4\trs"+rs_id+"\t"+leftHit.size()+"\t"+rightHit.size());
			
			challenge(leftHit, rightHit,rs_id);
			}
		c.close();
		c= this.right.openWalker(null);
		while(c.nextNoDup())
			{
			int rs_id = c.getKey();
			if(this.left.containsKey(null,rs_id)) continue;
			rightHit.clear();
			rightHit.addAll(this.right.get(null, rs_id));
			
			System.out.println("#4\trs"+rs_id+"\t0\t"+rightHit.size());
			
			alone(rightHit,'R',rs_id);
			}
		c.close();
		}
	

	
	private void alone(
			List<Hit> hits,
			char side,
			int rs_id
			)
		{
		for(Hit h:hits)
			{
			System.out.println("#3\trs"+rs_id+"\t"+
				side+"\t"+
				int2chrom(h.chrom_id)+"\t"+
				h.strand+"\t"+
				h.position+"\t"+
				h.length+"\t"+
				h.mismatch
				);
			}
		}
	
	private static void push_candidate(CandidatePair candidate,List<CandidatePair> candidates)
		{
		if(candidates.isEmpty())
			{
			candidates.add(candidate);
			return;
			}
			
		int cmp= candidates.get(0).compareTo( candidate);
		if(cmp > 0)
			{
			candidates.clear();
			candidates.add(candidate);
			return;
			}
		else if(cmp<=0)
			{
			return;
			}
		
		candidates.add(candidate);
		Collections.sort(candidates);
		}
	
	private void challenge(
			List<Hit> leftHit,
			List<Hit> rightHit,
			int rs_id
		)
		{
		if(leftHit.isEmpty() && !rightHit.isEmpty())
			{
			alone(rightHit,'R',rs_id);
			return;
			}
		else if(!leftHit.isEmpty() && rightHit.isEmpty())
			{
			alone(leftHit,'L',rs_id);
			return;
			}
			
		List<CandidatePair> candidates= new ArrayList<CandidatePair>();
		for(int i=0;i< leftHit.size();++i)
			{
			Hit L= leftHit.get(i);
			for(int j=0;j< rightHit.size();++j)
				{
				Hit R= rightHit.get(j);
				if(R.strand!=L.strand) continue;
				if(R.chrom_id!=L.chrom_id) continue;
				if(R.strand=='+')
					{
					if(R.position < L.position+L.length) continue;
					if(R.position - (L.position+L.length) > max_len_deletion) continue;
					CandidatePair candidate= new CandidatePair(L,R);
					push_candidate(candidate,candidates);
					}
				else if(R.strand=='-')
					{
					if(L.position < R.position+R.length) continue;
					if(L.position - (R.position+R.length) > max_len_deletion) continue;
					CandidatePair candidate= new CandidatePair(L,R);
					push_candidate(candidate,candidates);
					}
				}
			}
		
		if(candidates.isEmpty())
			{
			System.out.println("#0\trs"+rs_id);
			}
		
		for(CandidatePair candidate:candidates)
			{
			System.out.print(candidates.size()==1?"#1\t":"#2\t");
			candidate.print(System.out);
			System.out.println();
			}
		}
	
	
	public static void main(String[] args)
		{
		DigestBowtie app=null;
		try
			{
			app=new DigestBowtie();
			int optind= app.processArgs(args);
			if(optind==args.length)
				{
				System.err.println("No arguments");
				return;
				}
			app.open();                   
            while(optind< args.length)
                    {
			System.err.println("Reading "+args[optind]);
                     BufferedReader r=IOUtils.openReader(args[optind++]);
                    app.parse(r);
                    r.close();
                    }
            app.dump();
			}
		catch (Exception e)
			{
			e.printStackTrace();
			}
		finally
			{
			if(app!=null) app.close();
			}
		}
	}
